diff -N -ur -x_darcs trac/trac/db_default.py trac+darcs/trac/db_default.py --- trac/trac/db_default.py 2005-11-17 12:26:10.838697816 +0100 +++ trac+darcs/trac/db_default.py 2005-11-17 12:26:34.999024888 +0100 @@ -82,6 +82,7 @@ Column('time', type='int'), Column('author'), Column('message'), + Column('hash'), Index(['time'])], Table('node_change', key=('rev', 'path', 'change'))[ Column('rev'), diff -N -ur -x_darcs trac/trac/env.py trac+darcs/trac/env.py --- trac/trac/env.py 2005-11-17 12:26:10.838697816 +0100 +++ trac+darcs/trac/env.py 2005-11-17 12:26:34.999024888 +0100 @@ -150,17 +150,8 @@ @param authname: user name for authorization """ - from trac.versioncontrol.cache import CachedRepository - from trac.versioncontrol.svn_authz import SubversionAuthorizer - from trac.versioncontrol.svn_fs import SubversionRepository - repos_dir = self.config.get('trac', 'repository_dir') - if not repos_dir: - raise EnvironmentError, 'Path to repository not configured' - authz = None - if authname: - authz = SubversionAuthorizer(self, authname) - repos = SubversionRepository(repos_dir, authz, self.log) - return CachedRepository(self.get_db_cnx(), repos, authz, self.log) + from trac.versioncontrol import get_repository + return get_repository(self, authname) def create(self, db_str=None): """Create the basic directory structure of the environment, initialize @@ -350,6 +341,20 @@ err = 'No upgrade module for version %i (%s.py)' % (i, name) raise TracError, err script.do_upgrade(self.env, i, cursor) + + # Allow micro-upgrades: this is mainly intended to allow third + # parties fixups + from string import ascii_lowercase + for l in ascii_lowercase: + microname = name + l + try: + upgrades = __import__('upgrades', globals(), locals(), + [microname]) + script = getattr(upgrades, microname) + except AttributeError: + break + script.do_upgrade(self.env, i, cursor) + cursor.execute("UPDATE system SET value=%s WHERE " "name='database_version'", (db_default.db_version,)) self.log.info('Upgraded database version from %d to %d', diff -N -ur -x_darcs trac/trac/upgrades/db14a.py trac+darcs/trac/upgrades/db14a.py --- trac/trac/upgrades/db14a.py 1970-01-01 01:00:00.000000000 +0100 +++ trac+darcs/trac/upgrades/db14a.py 2005-11-17 12:26:35.048017440 +0100 @@ -0,0 +1,16 @@ +sql = [ +#-- Make the revision contain an hash, and suggest a resync +"""DROP TABLE revision;""", +"""CREATE TABLE revision ( + rev text PRIMARY KEY, + time integer, + author text, + message text, + hash text +);""", +] + +def do_upgrade(env, ver, cursor): + for s in sql: + cursor.execute(s) + print 'Please perform a "resync" after this upgrade.' diff -N -ur -x_darcs trac/trac/versioncontrol/api.py trac+darcs/trac/versioncontrol/api.py --- trac/trac/versioncontrol/api.py 2005-11-17 12:26:10.863694016 +0100 +++ trac+darcs/trac/versioncontrol/api.py 2005-11-17 12:26:35.052016832 +0100 @@ -210,13 +210,32 @@ def get_changes(self): """ - Generator that produces a (path, kind, change, base_rev, base_path) + Generator that produces a (path, kind, change, base_path, base_rev) tuple for every change in the changeset, where change can be one of Changeset.ADD, Changeset.COPY, Changeset.DELETE, Changeset.EDIT or Changeset.MOVE, and kind is one of Node.FILE or Node.DIRECTORY. """ raise NotImplementedError + def insert_in_cache(self, cursor, kindmap, actionmap, log): + """ + Insert this changeset in the cache db. + """ + cursor.execute("INSERT INTO revision (rev,time,author,message) " + "VALUES (%s,%s,%s,%s)", (str(self.rev), + self.date, self.author, + self.message)) + for path,kind,action,base_path,base_rev in self.get_changes(): + log.debug("Caching node change in [%s]: %s" + % (self.rev, (path, kind, action, + base_path, base_rev))) + kind = kindmap[kind] + action = actionmap[action] + cursor.execute("INSERT INTO node_change (rev,path,kind," + "change,base_path,base_rev) " + "VALUES (%s,%s,%s,%s,%s,%s)", + (str(self.rev), path, kind, action, + base_path, base_rev)) class PermissionDenied(PermissionError): """ @@ -249,3 +268,55 @@ def has_permission_for_changeset(self, rev): return 1 + +def get_repository(env, authname): + """ + Return the right repository backend wrapped by a CachedRepository. + + This looks for a ``darcs:`` or ``bzr:`` prefix on the + 'repository_dir' from the configuration: if present it qualifies + respectively a Darcs or a Bazaar-NG repository, otherwise it uses + the Subversion backend. + """ + + authz = None + repos_dir = env.config.get('trac', 'repository_dir') + db = env.get_db_cnx() + if not repos_dir: + raise EnvironmentError, 'Path to repository not configured' + if repos_dir.startswith('darcs:'): + from trac.versioncontrol.darcs import DarcsRepository, \ + DarcsCachedRepository as CachedRepository + repos = DarcsRepository(db, repos_dir[6:], env.log, env.config) + elif repos_dir.startswith('bzr:'): + from trac.versioncontrol.bzr import BzrRepository, \ + BzrCachedRepository as CachedRepository + #from trac.versioncontrol.cache import CachedRepository + repos = BzrRepository(db, repos_dir[4:], env.log) + else: + from trac.versioncontrol.svn_authz import SubversionAuthorizer + from trac.versioncontrol.svn_fs import SubversionRepository + from trac.versioncontrol.cache import CachedRepository + if authname: + authz = SubversionAuthorizer(env, authname) + repos = SubversionRepository(repos_dir, authz, env.log) + return CachedRepository(db, repos, authz, env.log) + + +def get_authorizer(env, authname): + """ + Return the right authorizer for the configured repository. + + Like ``get_repository()`` this looks for a ``darcs:`` or ``bzr:`` + prefix on the 'repository_dir' setting: if present it returns + a basic Authorizer(), otherwise a SubversionAuthorizer. + """ + + authz = Authorizer() + repos_dir = env.config.get('trac', 'repository_dir') + if not repos_dir.startswith('darcs:') \ + and not repos_dir.startswith('bzr:'): + from trac.versioncontrol.svn_authz import SubversionAuthorizer + if authname: + authz = SubversionAuthorizer(env, authname) + return authz diff -N -ur -x_darcs trac/trac/versioncontrol/bzr.py trac+darcs/trac/versioncontrol/bzr.py --- trac/trac/versioncontrol/bzr.py 1970-01-01 01:00:00.000000000 +0100 +++ trac+darcs/trac/versioncontrol/bzr.py 2005-11-17 12:26:35.186996312 +0100 @@ -0,0 +1,383 @@ +# -*- coding: iso-8859-1 -*- +# +# Copyright (C) 2005 Edgewall Software +# Copyright (C) 2005 Johan Rydberg +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.com/license.html. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://projects.edgewall.com/trac/. +# +# Author: Johan Rydberg + +from __future__ import generators + +from trac.util import TracError, NaivePopen +from trac.versioncontrol import Changeset, Node, Repository +from trac.versioncontrol.cache import CachedRepository, CachedChangeset + +import os + + +class BzrRepository(Repository): + """ + A Bazaar-NG repository. + """ + def __init__(self, db, path, log): + from bzrlib.branch import Branch + self.branch = Branch.open(path) + self.db = db + Repository.__init__(self, path, None, log) + + def get_base_rev(self, file_id, start_rev = None): + """ + Lookup what revision file with file id specified by file_id was + last modified by. start_rev gives the revision where to start + scanning from (in reverese order.) start_rev is _EXCLUSIVE_. + """ + def _enumerate_history(branch): + rh = [] + revno = 1 + for rev_id in branch.revision_history(): + rh.append((revno, rev_id)) + revno += 1 + return rh + + if start_rev: + start_rev = self.normalize_rev(start_rev) + if start_rev and start_rev <= 1: + return 0 + + which_revs = _enumerate_history(self.branch) + if start_rev: + which_revs = which_revs[:start_rev - 1] + + which_revs.reverse() + + for revno, rev_id in which_revs: + delta = self.branch.get_revision_delta(revno) + + if delta.touches_file_id(file_id): + return revno + + return 0 + + def get_oldest_rev(self): + """ + Revisions are numbered from 1 to the highest revision number, + return by branch.revno(). + """ + return 1 + + def get_youngest_rev(self): + """ + Return the youngest revision in the repository. + """ + return self.branch.revno() + + def get_kind(self, path, rev): + """ + Determine the kind of the path at given revision. + """ + + kind = 'F' + if not path.endswith('/') and path != "": + cursor = self.db.cursor() + cursor.execute("SELECT kind " + "FROM node_change " + "WHERE ((LENGTH(rev) self.get_youngest_rev(): + rev = self.get_youngest_rev() + return rev + + def previous_rev(self, rev): + """ + Return the revision immediately preceding the specified revision. + """ + rev = self.normalize_rev(rev) + if rev == 1: + return None + else: + return rev-1 + + def next_rev(self, rev): + """ + Return the revision immediately following the specified revision. + """ + rev = self.normalize_rev(rev) + if rev < self.get_youngest_rev(): + return rev+1 + else: + return None + + def rev_older_than(self, rev1, rev2): + """ + Return True if rev1 is older than rev2, i.e. if rev1 comes before rev2 + in the revision sequence. + """ + return self.normalize_rev(rev1) < self.normalize_rev(rev2) + +class BzrNode(Node): + def __init__(self, repo, path, kind, rev): + Node.__init__(self, path, rev, kind) + self.repo = repo + revision_id = repo.branch.get_rev_id(rev) + self.tree = repo.branch.revision_tree(revision_id) + + def get_history(self, limit=None): + """ + Generator that yields (path, rev, chg) tuples, one for each + revision in which the node was changed. This generator will + follow copies and moves of a node (if the underlying version + control system supports that), which will be indicated by the + first element of the tuple (i.e. the path) changing. + """ + + cursor = self.repo.db.cursor() + + rev = self.rev + path = self.path + + actions = { 'A': Changeset.ADD, + 'E': Changeset.EDIT, + 'M': Changeset.MOVE, + 'D': Changeset.DELETE } + + if self.kind == Node.DIRECTORY: + revdone = {} + cursor.execute("SELECT rev,change,base_path" + " FROM node_change " + "WHERE ((LENGTH(rev)=1: + cursor.execute("SELECT kind,change,base_path,base_rev " + "FROM node_change WHERE rev=%d AND path=%s", + (rev, path)) + base_rev = None + row = cursor.fetchone() + if row: + kind, change, base_path, base_rev = row + yield path, rev, actions[change] + if limit: + limit -= 1 + if limit == 0: + break + base_rev = base_rev and int(base_rev) or 0 + path = base_path + + if base_rev is None: + rev -= 1 + else: + rev = int(base_rev) + + def get_content(self): + """ + Return a stream for reading the content of the node. This method + will return None for directories. The returned object should provide + a read([len]) function. + """ + return self.tree.get_file(self.tree.inventory.path2id(self.path)) + + def get_entries(self): + """ + Generator that yields the immediate child entries of a directory, in no + particular order. If the node is a file, this method returns None. + """ + if self.kind == Node.DIRECTORY: + file_id = self.tree.inventory.path2id(self.path) + ie = self.tree.inventory[file_id] + for x, kid in ie.sorted_children(): + yield self.repo.get_node(os.path.join(self.path, kid.name), + self.rev) + + def get_name(self): + return os.path.split(self.path)[1] + + def get_content_length(self): + if self.kind == Node.DIRECTORY: + return None + + file_id = self.tree.inventory.path2id(self.path) + ie = self.tree.inventory[file_id] + return ie.text_size + + def get_content_type(self): + if self.kind == Node.DIRECTORY: + return None + # FIXME: guess type here. + return None + + def get_last_modified(self): + # FIXME: add + return 0 + + def get_properties(self): + """ + Returns a dictionary containing the properties (meta-data) of the node. + The set of properties depends on the version control system. + """ + return {} + + +class BzrChangeset(Changeset): + def __init__(self, repo, rev, revno): + """ + Initialize bazaar changeset. rev is the revision object provided + by bzrlib, revno is its revision number. + """ + self.revobj = rev + self.repo = repo + Changeset.__init__(self, revno, rev.message.encode('utf-8'), + rev.committer, rev.timestamp) + + def get_changes(self): + """ + Generator that produces a (path, kind, change, base_path, base_rev) + tuple for every change in the changeset, where change can be one of + Changeset.ADD, Changeset.COPY, Changeset.DELETE, Changeset.EDIT or + Changeset.MOVE, and kind is one of Node.FILE or Node.DIRECTORY. + """ + delta = self.repo.branch.get_revision_delta(self.rev) + + kinds = { 'file': Node.FILE, 'directory': Node.DIRECTORY, + 'root_directory': Node.DIRECTORY } + + for path, file_id, kind in delta.added: + base_rev = self.repo.get_base_rev(file_id, self.rev) + yield path, kinds[kind], Changeset.ADD, path, base_rev + + for path, file_id, kind, _, _ in delta.modified: + base_rev = self.repo.get_base_rev(file_id, self.rev) + yield path, kinds[kind], Changeset.EDIT, path, base_rev + + for path, file_id, kind in delta.removed: + base_rev = self.repo.get_base_rev(file_id, self.rev) + yield path, kinds[kind], Changeset.DELETE, path, base_rev + + for old_path, new_path, file_id, kind, modified, _ in delta.renamed: + base_rev = self.repo.get_base_rev(file_id, self.rev) + yield new_path, kinds[kind], Changeset.MOVE, old_path, base_rev + +class BzrCachedRepository(CachedRepository): + """ + Bazaar-NG version of the cached repository, that serves BzrCachedChangesets + """ + + def get_changeset(self, rev): + if not self.synced: + self.sync() + self.synced = 1 + return CachedChangeset(self.repos.normalize_rev(rev), self.db, + self.authz) + diff -N -ur -x_darcs trac/trac/versioncontrol/cache.py trac+darcs/trac/versioncontrol/cache.py --- trac/trac/versioncontrol/cache.py 2005-11-17 12:26:10.863694016 +0100 +++ trac+darcs/trac/versioncontrol/cache.py 2005-11-17 12:26:35.053016680 +0100 @@ -76,21 +76,7 @@ current_rev = self.repos.oldest_rev while current_rev is not None: changeset = self.repos.get_changeset(current_rev) - cursor.execute("INSERT INTO revision (rev,time,author,message) " - "VALUES (%s,%s,%s,%s)", (str(current_rev), - changeset.date, changeset.author, - changeset.message)) - for path,kind,action,base_path,base_rev in changeset.get_changes(): - self.log.debug("Caching node change in [%s]: %s" - % (current_rev, (path, kind, action, - base_path, base_rev))) - kind = kindmap[kind] - action = actionmap[action] - cursor.execute("INSERT INTO node_change (rev,path,kind," - "change,base_path,base_rev) " - "VALUES (%s,%s,%s,%s,%s,%s)", - (str(current_rev), path, kind, action, - base_path, base_rev)) + changeset.insert_in_cache(cursor, kindmap, actionmap, self.log) current_rev = self.repos.next_rev(current_rev) self.db.commit() self.repos.authz = authz # restore permission checking diff -N -ur -x_darcs trac/trac/versioncontrol/darcs.py trac+darcs/trac/versioncontrol/darcs.py --- trac/trac/versioncontrol/darcs.py 1970-01-01 01:00:00.000000000 +0100 +++ trac+darcs/trac/versioncontrol/darcs.py 2005-11-17 12:26:35.187996160 +0100 @@ -0,0 +1,951 @@ +# -*- coding: iso-8859-1 -*- +# +# Copyright (C) 2005 Edgewall Software +# Copyright (C) 2005 Lele Gaifax +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.com/license.html. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://projects.edgewall.com/trac/. +# +# Author: Lele Gaifax + +from __future__ import generators + +from trac.util import TracError, NaivePopen +from trac.versioncontrol import Changeset, Node, Repository +from trac.versioncontrol.cache import CachedRepository, CachedChangeset +from os import listdir, makedirs, utime, stat +from os.path import join, isdir, split, exists +from time import gmtime, mktime, strftime, timezone +from shutil import copyfile +from mimetypes import guess_type + +# Python 2.3+ compatibility +try: + reversed +except: + def reversed(x): + if hasattr(x, 'keys'): + raise ValueError("mappings do not support reverse iteration") + i = len(x) + while i > 0: + i -= 1 + yield x[i] + +class DarcsRepository(Repository): + """ + Darcs concrete implementation of a Repository. + + Darcs (http://www.abridgegame.org/darcs/) is a distribuited SCM, + patch-centric instead of snapshot-centric like Subversion. The + approach here is to assume that the history in the repository + followed by Trac is immutable, which is not a requirement for the + underlaying darcs. This means that on that repository should never + be executed a `darcs unpull`, or `darcs unrecord` and the like. + + Given it's nature, this class tries to cache as much as possible + the requests coming from the above Trac machinery, to avoid or + minimize the external calls to Darcs that tend to be somewhat time + expensive. In particular, _getNodeContent() caches any particular + version of any file Trac asks to it, kept on the filesystem, + inside the ``_darcs`` metadir of the controlled repository in the + directory ``trac_cache`` (or where specified by the option + ``cachedir`` in the section ``darcs`` of the configuration. This + may require some kind of control over the size of the cache, that + OTOH may be completely removed whenever needed, it will be + recreated as soon as the first request come in. + """ + + def __init__(self, db, path, log, config): + Repository.__init__(self, path, None, log) + self.path = path + self.db = db + self.__youngest_rev = 0 + self.__history = None + self.__history_start = 0 + self.__no_pristine = exists(join(self.path, '_darcs', 'current.none')) + self.__darcs = config.get('darcs', 'command', 'darcs') + if config.get('darcs', 'dont_escape_8bit'): + self.__darcs = "DARCS_DONT_ESCAPE_8BIT=1 " + self.__darcs + self.__cachedir = config.get('darcs', 'cachedir', + join(self.path, '_darcs', 'trac_cache')) + + ## Low level stuff: CamelCase is used to avoid clash with inherited + ## interface namespace. + + def _darcs (self, command, args=''): + """ + Execute a some query on the repository running an external darcs. + + Return the XML output of the command. + """ + + command = "cd %r; TZ=UTC %s %s %s" % \ + (self.path, self.__darcs, command, args) + self.log.debug(command) + np = NaivePopen (command, capturestderr=True) + if np.errorlevel: + err = 'Running (%s) failed: %s, %s.' % (command, + np.errorlevel, np.err) + raise TracError (err, title='Darcs execution failed') + return np.out + + def _changes(self, args, startfrom=1): + """ + Get changes information parsing the XML output of ``darcs changes``. + + Return a sequence of Changesets. + """ + + return changesets_from_darcschanges( + self._darcs('changes --reverse --summary --xml', args), + self, startfrom) + + def _diff(self, path, rev1, rev2=None, patch=None): + """ + Return a darcs diff between two revisions. + """ + + diff = "diff --unified" + rev1 = self.normalize_rev(rev1) + cset1 = DarcsCachedChangeset(rev1, self.db) + diff += " --from-match 'hash %s'" % cset1.hash + if rev2: + rev2 = self.normalize_rev(rev2) + cset2 = DarcsCachedChangeset(rev2, self.db) + diff += " --to-match 'hash %s'" % cset2.hash + if patch: + path = path + patch + return self._darcs(diff, path) + + def _parseInventory(self, inventory): + """ + Parse an inventory, and return a dictionary of its content. + """ + + from sha import new + + csmap = {} + start = 0 + length = len(inventory) + index = self.__youngest_rev + while start ']': + start = end + end = inventory.index('\n', start+1) + while True: + log += inventory[start+2:end] + start = end + if inventory[end+1] == ']': + break + end = inventory.index('\n', start+1) + end += 1 + + index += 1 + phash = new() + phash.update(patchname) + phash.update(author) + phash.update(date) + phash.update(log) + phash.update(inv) + patchid = '%s-%s-%s.gz' % (date, + new(author).hexdigest()[:5], + phash.hexdigest()) + csmap[index] = patchid + csmap[patchid] = index + start = end+1 + self.__youngest_rev = index + return csmap + + def _loadChangesetsIndex(self): + """ + Load the index of changesets in the repository, assigning an unique + integer number to each one, ala Subversion, for easier references. + + This is done by parsing the ``_darcs/inventory`` file using the + position of each changeset as its `revision number`, **assuming** + that **nobody's never** going to do anything that alters its order, + such as ``darcs optimize`` or ``darcs unpull``. + """ + + self.__youngest_rev = 0 + inventories = [join(self.path, '_darcs', 'inventories', i) for i + in listdir(join(self.path, '_darcs', 'inventories'))] + inventories.sort() + inventories.append(join(self.path, '_darcs', 'inventory')) + for inventory in inventories: + f = open(inventory, 'rU') + try: + index = self._parseInventory(f.read()) + finally: + f.close() + + ## Medium level, work-horse methods + + def _getCachedContentLocation(self, node): + """ + Return the location of the cache for the given node. If it does + not exist, compute it by applying a diff to the current version. + This may return None, if the node does not actually exist. + """ + + rev = self.normalize_rev(node.rev) + + if self.__no_pristine: + current = join(self.path, node.path) + else: + current = join(self.path, '_darcs', 'current', node.path) + + # Iterate over history to find out which is the revision of + # the given path that last changed the it. We need to find + # both a 'last revision' and 'second last', because later + # we may apply either a r1:last diff or a 2nd:current diff. + history = self._getPathHistory(node.path, None) + try: + lastnode = history.next() + except StopIteration: + lastnode = None + + if lastnode is None: + return None + elif lastnode.rev <= rev: + # Content hasn't changed, return current version + if exists(current): + return current + + prevlast = lastnode + for oldnode in history: + if oldnode.rev <= rev: + lastnode = oldnode + break + prevlast = oldnode + + cachedir = join(self.__cachedir, str(lastnode.rev)) + cache = join(cachedir, lastnode.path) + + # One may never know: should by any chance an absolute path survived + # in lastnode.path, or in some clever way introduced some trick like + # 'somepath/../../../etc/passwd'... + from os.path import normpath + assert normpath(cache).startswith(cachedir) + + if not exists(cache): + self.log.debug('Caching revision %d of %s' % (lastnode.rev, + lastnode.path)) + dir = split(cache)[0] + if not exists(dir): + makedirs(dir) + + # If the file doesn't current exist, create an empty file + # and apply a patch from revision 1 to the node revision, + # otherwise apply a reversed patch from the current revision + # and to node revision+1. + try: + if not exists(current): + self.log.debug('Applying a direct patch from revision 1 up' + ' to %d to %s' % (lastnode.rev, node.path)) + open(cache, 'w').close() + patch = "| patch -p1 -d %s" % cachedir + self._diff(node.path, 1, lastnode.rev, patch=patch) + else: + self.log.debug('Applying a reverse patch from current' + ' revision back to %d to %s' % + (lastnode.rev, node.path)) + copyfile(current, cache) + patch = "| patch -p1 -R -d %s" % cachedir + self._diff(node.path, prevlast.rev, patch=patch) + except TracError, exc: + if 'Only garbage was found in the patch input' in exc.message: + pass + else: + raise + + # Adjust the times of the just created cache file, to match + # the timestamp of the associated changeset. + cursor = self.db.cursor() + cursor.execute("SELECT time FROM revision " + "WHERE rev = %s", (lastnode.rev,)) + cstimestamp = int(cursor.fetchone()[0]) + utime(cache, (cstimestamp, cstimestamp)) + + if exists(cache): + return cache + else: + return None + + def _getNodeContent(self, node): + """ + Return the content of the node, loading it from the cache. + """ + + from cStringIO import StringIO + + location = self._getCachedContentLocation(node) + if location: + return file(location) + else: + return StringIO('') + + def _getNodeSize(self, node): + """ + Return the content of the node, loading it from the cache. + """ + + location = self._getCachedContentLocation(node) + if location: + return stat(location).st_size + else: + return None + + def _getNodeEntries(self, node): + """ + Generate the the immediate child entries of a directory at given + revision, in alpha order. + """ + + from cache import _actionmap + + # Loop over nodes touched before given rev that falls in the + # given path. We effectively want to look at the whole subtree, + # because when a child is a directory we annotate it with the + # latest change happened below that, instead with the revision + # that actually touched the directory itself. + + cursor = self.db.cursor() + path = node.path.strip('/') + cursor.execute("SELECT rev, path, change, base_path " + " FROM node_change " + "WHERE ((LENGTH(rev)self.__history_start: + node = None + for cs in reversed(self.__history): + if cs.rev > rev: + continue + node = cs.get_node(path) + if node: + yield node + if limit: + limit -= 1 + if limit==0: + break + # Expand renames + if node.change == Changeset.MOVE: + for node in self._getPathHistory(node.oldpath, + node.rev-1, limit): + yield node + if limit: + limit -= 1 + if node is not None: + rev = node.rev-1 + + # Keep going with the cache stored in the DB + kind = self._getNodeKind(path, rev) + cursor = self.db.cursor() + + path = path.rstrip('/') + if kind == Node.DIRECTORY: + revdone = {} + cursor.execute("SELECT rev,kind,change,base_path" + " FROM node_change " + "WHERE ((LENGTH(rev)=1: + cursor.execute("SELECT kind,change,base_path,base_rev " + "FROM node_change WHERE rev=%s AND path=%s", + (rev, path)) + base_rev = None + for row in cursor: + kind, change, base_path, base_rev = row + node = DarcsNode(path, rev, _kindmap[kind], + _actionmap[change], self, + oldpath=base_path) + yield node + if limit: + limit -= 1 + if limit==0: + break + base_rev = base_rev and int(base_rev) or 0 + # Expand renames + if node.change == Changeset.MOVE: + for node in self._getPathHistory(node.oldpath, + base_rev, limit): + yield node + if limit: + limit -= 1 + + if base_rev is None: + rev -= 1 + else: + rev = base_rev + + def _getNodeKind(self, path, rev): + """ + Determine the kind of the path at given revision. + """ + + # Determine if the path is really a directory, except when it's + # already known: it is, when its name ends with a slash (a fake + # one introduced by changesets_from_darcschanges()) or it is the + # empty string, resulted from normalize_path('/'). + if not path.endswith("/") and path <> "": + cursor = self.db.cursor() + cursor.execute("SELECT path " + "FROM node_change " + "WHERE ((LENGTH(rev) self.get_youngest_rev(): + rev = self.get_youngest_rev() + return rev + + +class DarcsChangeset(Changeset): + """ + Represents a set of changes of a repository. + """ + + def __init__(self, rev, patchname, message, author, date, changes, hash): + if message: + log = patchname + '\n' + message + else: + log = patchname + Changeset.__init__(self, rev, log, author, date) + self.patchname = patchname + self.changes = changes + self.hash = hash + # fix up changes rev slot + for c in self.changes: + c.rev = rev + + def get_changes(self): + """ + Generator that produces a (path, kind, change, base_path, base_rev) + """ + + moves = {} + for c in self.changes: + last = c.get_history(limit=2) + try: + last.next() + basepath,baserev,basechg = last.next() + except StopIteration: + basepath = None + baserev = -1 + yield (c.path, c.kind, c.change, c.oldpath or basepath, baserev) + + def get_node(self, path, maybedir=False): + """ + Find and return the node relative to given path. + """ + + for c in self.changes: + if c.path == path or c.oldpath == path: + return c + if maybedir and not path.endswith('/'): + path += '/' + if c.path == path or c.oldpath == path: + return c + + def insert_in_cache(self, cursor, kindmap, actionmap, log): + """ + Augment standard metadata with darcs patch hash. + """ + + Changeset.insert_in_cache(self, cursor, kindmap, actionmap, log) + cursor.execute("UPDATE revision SET hash = %s " + "WHERE rev = %s", (self.hash, self.rev)) + + +class DarcsNode(Node): + """ + Represent a single item changed within a Changeset. + """ + + def __init__(self, path, rev, kind, change, repository, oldpath=None): + Node.__init__(self, path, rev, kind) + self.change = change + self.repository = repository + self.oldpath = oldpath + + def __cmp__(self, other): + res = cmp(self.rev, other.rev) + if res: + return res + res = cmp(self.path, other.path) + if res == 0: + if self.change==Changeset.MOVE and other.change==Changeset.DELETE: + res = 1 + elif self.change==Changeset.DELETE and other.change==Changeset.MOVE: + res = -1 + return res + + def get_content(self): + """ + Return a stream for reading the content of the node. This method + will return None for directories. The returned object should provide + a read([len]) function. + """ + + if self.isdir: + return None + + return self.repository._getNodeContent(self) + + def get_entries(self): + """ + Generator that yields the immediate child entries of a directory, in no + particular order. If the node is a file, this method returns None. + """ + + if self.isdir: + return self.repository._getNodeEntries(self) + + def get_history(self, limit=None): + """ + Generator that yields (path, rev, chg) tuples, one for each + revision in which the node was changed. This generator will + follow copies and moves of a node (if the underlying version + control system supports that), which will be indicated by the + first element of the tuple (i.e. the path) changing. + """ + + # Start with current version + yield (self.path, self.rev, self.change) + + # Keep going with the previous steps, possibly following the old + # name of the entry if this is a move. + prevpath = self.oldpath or self.path + prevrev = self.repository.normalize_rev(self.rev)-1 + prevhist = self.repository.get_path_history(prevpath, prevrev, limit-1) + for path, rev, chg in prevhist: + yield (path, rev, chg) + + def get_properties(self): + """ + Returns a dictionary containing the properties (meta-data) of the node. + The set of properties depends on the version control system. + """ + + return {} + + def get_content_length(self): + if self.isdir: + return None + return self.repository._getNodeSize(self) + + def get_content_type(self): + if self.isdir: + return None + return guess_type(self.path)[0] + + def get_name(self): + return split(self.path)[1] + + def get_last_modified(self): + return self.repository._getNodeLastModified(self) + + +def changesets_from_darcschanges(changes, repository, start_revision): + """ + Parse XML output of ``darcs changes``. + + Return a list of ``Changeset`` instances. + """ + + from xml.sax import parseString, SAXException + from xml.sax.handler import ContentHandler + + class DarcsXMLChangesHandler(ContentHandler): + def __init__(self): + self.changesets = [] + self.index = start_revision-1 + self.current = None + self.current_field = [] + + def startElement(self, name, attributes): + if name == 'patch': + self.current = {} + self.current['author'] = attributes['author'] + date = attributes['date'] + # 20040619130027 + y = int(date[:4]) + m = int(date[4:6]) + d = int(date[6:8]) + hh = int(date[8:10]) + mm = int(date[10:12]) + ss = int(date[12:14]) + unixtime = int(mktime((y, m, d, hh, mm, ss, 0, 0, 0)))-timezone + self.current['date'] = unixtime + self.current['comment'] = '' + self.current['hash'] = attributes['hash'] + self.current['entries'] = [] + elif name in ['name', 'comment', 'add_file', 'add_directory', + 'remove_directory', 'modify_file', 'remove_file']: + self.current_field = [] + elif name == 'move': + self.old_name = attributes['from'] + self.new_name = attributes['to'] + + def endElement(self, name): + if name == 'patch': + # Sort the paths to make tests easier + self.current['entries'].sort() + self.index += 1 + cset = DarcsChangeset(self.index, + self.current['name'], + self.current['comment'], + self.current['author'], + self.current['date'], + self.current['entries'], + self.current['hash']) + self.changesets.append(cset) + self.current = None + elif name in ['name', 'comment']: + self.current[name] = ''.join(self.current_field) + elif name == 'move': + kind = None + for cs in reversed(self.changesets): + node = cs.get_node(self.old_name, maybedir=True) + if node: + kind = node.kind + break + if kind is None: + kind = repository._getNodeKind(self.old_name, self.index) + if kind == Node.DIRECTORY: + self.new_name += '/' + self.old_name += '/' + entry = DarcsNode(self.new_name, None, kind, Changeset.MOVE, + repository, self.old_name) + self.current['entries'].append(entry) + elif name in ['add_file', 'add_directory', 'modify_file', + 'remove_file', 'remove_directory']: + path = ''.join(self.current_field).strip() + change = { 'add_file': Changeset.ADD, + 'add_directory': Changeset.ADD, + 'modify_file': Changeset.EDIT, + 'remove_file': Changeset.DELETE, + 'remove_directory': Changeset.DELETE + }[name] + isdir = name in ('add_directory', 'remove_directory') + kind = isdir and Node.DIRECTORY or Node.FILE + # Eventually add one final '/' to identify directories. + # This is because Trac brings around simple tuples at times, + # that cannot carry that flag with them. + if isdir: + path += '/' + entry = DarcsNode(path, None, kind, change, repository) + self.current['entries'].append(entry) + + def characters(self, data): + self.current_field.append(data) + + handler = DarcsXMLChangesHandler() + try: + parseString(changes, handler) + except SAXException, le: + raise TracError('Unable to parse "darcs changes" output: ' + str(le)) + + return handler.changesets + +class DarcsCachedRepository(CachedRepository): + """ + Darcs version of the cached repository, that serves DarcsCachedChangesets + """ + + def get_changeset(self, rev): + if not self.synced: + self.sync() + self.synced = 1 + return DarcsCachedChangeset(self.repos.normalize_rev(rev), self.db, + self.authz) + +class DarcsCachedChangeset(CachedChangeset): + """ + Darcs version of the CachedChangeset that knows about the hash. + """ + + def __init__(self, rev, db, authz=None): + CachedChangeset.__init__(self, rev, db, authz) + cursor = self.db.cursor() + cursor.execute("SELECT hash FROM revision " + "WHERE rev=%s", (rev,)) + row = cursor.fetchone() + if row: + self.hash = row[0] diff -N -ur -x_darcs trac/trac/versioncontrol/web_ui/changeset.py trac+darcs/trac/versioncontrol/web_ui/changeset.py --- trac/trac/versioncontrol/web_ui/changeset.py 2005-11-17 12:26:10.871692800 +0100 +++ trac+darcs/trac/versioncontrol/web_ui/changeset.py 2005-11-17 12:26:35.081012424 +0100 @@ -25,8 +25,7 @@ from trac.perm import IPermissionRequestor from trac.Search import ISearchSource, query_to_sql, shorten_result from trac.Timeline import ITimelineEventProvider -from trac.versioncontrol import Changeset, Node -from trac.versioncontrol.svn_authz import SubversionAuthorizer +from trac.versioncontrol import Changeset, Node, get_authorizer from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff from trac.web import IRequestHandler from trac.web.chrome import add_link, add_stylesheet, INavigationContributor @@ -64,7 +63,7 @@ rev = req.args.get('rev') repos = self.env.get_repository(req.authname) - authzperm = SubversionAuthorizer(self.env, req.authname) + authzperm = get_authorizer(self.env, req.authname) authzperm.assert_permission_for_changeset(rev) diff_options = get_diff_options(req) @@ -106,7 +105,7 @@ 'changeset_show_files')) db = self.env.get_db_cnx() repos = self.env.get_repository() - authzperm = SubversionAuthorizer(self.env, req.authname) + authzperm = get_authorizer(self.env, req.authname) rev = repos.youngest_rev while rev: if not authzperm.has_permission_for_changeset(rev): @@ -383,7 +382,7 @@ def get_search_results(self, req, query, filters): if not 'changeset' in filters: return - authzperm = SubversionAuthorizer(self.env, req.authname) + authzperm = get_authorizer(self.env, req.authname) db = self.env.get_db_cnx() sql = "SELECT rev,time,author,message " \ "FROM revision WHERE %s" % \ diff -N -ur -x_darcs trac/wiki-default/TracIni trac+darcs/wiki-default/TracIni --- trac/wiki-default/TracIni 2005-11-17 12:26:10.894689304 +0100 +++ trac+darcs/wiki-default/TracIni 2005-11-17 12:26:35.154001328 +0100 @@ -88,6 +88,11 @@ == [wiki] == || `ignore_missing_pages` || enable/disable highlighting CamelCase links to missing pages (''since 0.9'') || +== [darcs] == +|| `cachedir` || By default the darcs backend keeps a cache of the visited files at various revision inside the repository itself, in `_darcs/trac_cache`, that may be overridden by this option, setting it to the desired directory that needs to be writeable by the trac process. || +|| `command` || Name of the external darcs executable, default to `darcs`. This can be used to set up the environment as well, like in "`DARCS_DONT_ESCAPE_ANYTHING=1 /usr/local/bin/darcs`" || +|| `dont_escape_8bit` || This is a shortcut for "`command=DARCS_DONT_ESCAPE_8BIT=1 darcs`" (true, false). Default to false. || + == [components] == (''since 0.9'')