| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2005 Edgewall Software |
|---|
| 4 | # Copyright (C) 2005-2010 Lele Gaifax <lele@metapensiero.it> |
|---|
| 5 | # |
|---|
| 6 | # This software is licensed as described in the file COPYING, which |
|---|
| 7 | # you should have received as part of this distribution. The terms |
|---|
| 8 | # are also available at http://trac.edgewall.com/license.html. |
|---|
| 9 | # |
|---|
| 10 | # This software consists of voluntary contributions made by many |
|---|
| 11 | # individuals. For the exact contribution history, see the revision |
|---|
| 12 | # history and logs, available at http://projects.edgewall.com/trac/. |
|---|
| 13 | # |
|---|
| 14 | # Author: Lele Gaifax <lele@metapensiero.it> |
|---|
| 15 | |
|---|
| 16 | from genshi.builder import tag |
|---|
| 17 | |
|---|
| 18 | from trac.admin import IAdminCommandProvider |
|---|
| 19 | from trac.config import BoolOption, Option |
|---|
| 20 | from trac.core import Component, implements |
|---|
| 21 | from trac.db import Column, DatabaseManager, Index, Table |
|---|
| 22 | from trac.env import IEnvironmentSetupParticipant |
|---|
| 23 | from trac.util.text import printout, shorten_line, to_unicode |
|---|
| 24 | from trac.versioncontrol import IRepositoryConnector, NoSuchChangeset, \ |
|---|
| 25 | RepositoryManager |
|---|
| 26 | from trac.versioncontrol.web_ui import IPropertyRenderer, RenderedProperty |
|---|
| 27 | |
|---|
| 28 | from trac.wiki import IWikiSyntaxProvider |
|---|
| 29 | |
|---|
| 30 | from tracdarcs.repository import DarcsRepository |
|---|
| 31 | |
|---|
| 32 | |
|---|
| 33 | class DarcsConnector(Component): |
|---|
| 34 | """Provide access to a darcs repository.""" |
|---|
| 35 | |
|---|
| 36 | implements(IRepositoryConnector, IWikiSyntaxProvider) |
|---|
| 37 | |
|---|
| 38 | dont_escape_8bit = BoolOption('darcs', 'dont_escape_8bit', 'false', |
|---|
| 39 | "Avoid darcs automatic escape of non-7bit chars.") |
|---|
| 40 | |
|---|
| 41 | darcs_command = Option('darcs', 'command', 'darcs', |
|---|
| 42 | "Name of the external darcs binary.") |
|---|
| 43 | |
|---|
| 44 | max_concurrent_darcses = Option('darcs', 'max_concurrent_darcses', 0, |
|---|
| 45 | "Max number of concurrent darcses running per " |
|---|
| 46 | "repository (0 means unlimited).") |
|---|
| 47 | |
|---|
| 48 | possible_encodings = Option('darcs', 'possible_encodings', 'utf-8,iso8859-1', |
|---|
| 49 | "Specify possible repository encodings.") |
|---|
| 50 | |
|---|
| 51 | eager_annotations = BoolOption('darcs', 'eager_annotations', 'false', |
|---|
| 52 | "Compute the annotation cache as soon as possible.") |
|---|
| 53 | |
|---|
| 54 | # IRepositoryConnector methods |
|---|
| 55 | |
|---|
| 56 | def get_supported_types(self): |
|---|
| 57 | """Support the `darcs:` scheme""" |
|---|
| 58 | yield ("darcs", 8) |
|---|
| 59 | |
|---|
| 60 | def get_repository(self, type, dir, params): |
|---|
| 61 | """Return a `DarcsRepository`""" |
|---|
| 62 | db = self.env.get_db_cnx() |
|---|
| 63 | darcs = self.darcs_command |
|---|
| 64 | if self.dont_escape_8bit: |
|---|
| 65 | darcs = "DARCS_DONT_ESCAPE_8BIT=1 " + darcs |
|---|
| 66 | if self.possible_encodings: |
|---|
| 67 | possible_encodings = [e.strip() |
|---|
| 68 | for e in self.possible_encodings.split(',')] |
|---|
| 69 | |
|---|
| 70 | # Setup the semaphore used to limit the number of concurrent running |
|---|
| 71 | # darcs within a single repository. |
|---|
| 72 | |
|---|
| 73 | if self.max_concurrent_darcses and int(self.max_concurrent_darcses)>0: |
|---|
| 74 | from command import DarcsCommand |
|---|
| 75 | if DarcsCommand.RUNNING_DARCSES is None: |
|---|
| 76 | from threading import BoundedSemaphore |
|---|
| 77 | DarcsCommand.RUNNING_DARCSES = BoundedSemaphore(value=int(self.max_concurrent_darcses)) |
|---|
| 78 | |
|---|
| 79 | return DarcsRepository(db, dir, self.env.log, darcs, possible_encodings, params, |
|---|
| 80 | self.eager_annotations) |
|---|
| 81 | |
|---|
| 82 | # IWikiSyntaxProvider methods |
|---|
| 83 | |
|---|
| 84 | def get_wiki_syntax(self): |
|---|
| 85 | yield (r'[0-9]{14}-[0-9a-f]{5}-[0-9a-f]{40}(.gz)?', |
|---|
| 86 | lambda formatter, label, match: self._format_link(formatter, 'cset', label, label)) |
|---|
| 87 | |
|---|
| 88 | def get_link_resolvers(self): |
|---|
| 89 | yield ('cset', self._format_link) |
|---|
| 90 | |
|---|
| 91 | def _format_link(self, formatter, ns, rev, label): |
|---|
| 92 | reponame = None |
|---|
| 93 | |
|---|
| 94 | # See if the context carries a repository... |
|---|
| 95 | context = formatter.context |
|---|
| 96 | while context: |
|---|
| 97 | if context.resource.realm in ('source', 'changeset'): |
|---|
| 98 | reponame = context.resource.parent.id |
|---|
| 99 | break |
|---|
| 100 | context = context.parent |
|---|
| 101 | |
|---|
| 102 | # If it does not, take the first repository containing the |
|---|
| 103 | # specified revision, if any. We can do this assuming that |
|---|
| 104 | # a) we are dealing with full darcs hashes and |
|---|
| 105 | # b) darcs hashes are globally unique |
|---|
| 106 | if reponame is None: |
|---|
| 107 | db = self.env.get_db_cnx() |
|---|
| 108 | c = db.cursor() |
|---|
| 109 | c.execute('SELECT r.value ' |
|---|
| 110 | 'FROM darcs_changesets c JOIN repository r ON c.repo_id = r.id ' |
|---|
| 111 | 'WHERE c.hash = %s AND r.name = %s ' |
|---|
| 112 | 'ORDER BY r.id ' |
|---|
| 113 | 'LIMIT 1', (rev, 'name',)) |
|---|
| 114 | row = c.fetchone() |
|---|
| 115 | reponame = row and row[0] or '' |
|---|
| 116 | |
|---|
| 117 | repos = self.env.get_repository(reponame) |
|---|
| 118 | if repos: |
|---|
| 119 | try: |
|---|
| 120 | chgset = repos.get_changeset(rev) |
|---|
| 121 | return tag.a(chgset.rev, class_="changeset", |
|---|
| 122 | title=shorten_line(chgset.message), |
|---|
| 123 | href=formatter.href.changeset(chgset.rev, reponame)) |
|---|
| 124 | except NoSuchChangeset, e: |
|---|
| 125 | errmsg = to_unicode(e) |
|---|
| 126 | else: |
|---|
| 127 | errmsg = 'Repository "%s" not found' % reponame |
|---|
| 128 | |
|---|
| 129 | return tag.a(label, class_="missing changeset", title=errmsg, rel="nofollow") |
|---|
| 130 | |
|---|
| 131 | |
|---|
| 132 | class DarcsSetup(Component): |
|---|
| 133 | """Setup darcs specific database tables.""" |
|---|
| 134 | |
|---|
| 135 | implements(IAdminCommandProvider, IEnvironmentSetupParticipant) |
|---|
| 136 | |
|---|
| 137 | def environment_created(self): |
|---|
| 138 | """After standard environment has been created, add the needed |
|---|
| 139 | tables.""" |
|---|
| 140 | |
|---|
| 141 | db = self.env.get_db_cnx() |
|---|
| 142 | self.upgrade_environment(db) |
|---|
| 143 | db.commit() |
|---|
| 144 | |
|---|
| 145 | def environment_needs_upgrade(self, db): |
|---|
| 146 | """Check to see if the darcs tables are already there, or need upgrade.""" |
|---|
| 147 | |
|---|
| 148 | debug = self.env.log.debug |
|---|
| 149 | |
|---|
| 150 | def check(table, stmt, should_fail=False): |
|---|
| 151 | c = db.cursor() |
|---|
| 152 | try: |
|---|
| 153 | try: |
|---|
| 154 | c.execute(stmt) |
|---|
| 155 | finally: |
|---|
| 156 | # Trac 1.0 connection wrapper hides 'rollback' for |
|---|
| 157 | # readonly stmts |
|---|
| 158 | db.cnx.rollback() |
|---|
| 159 | except Exception, e: |
|---|
| 160 | if should_fail: |
|---|
| 161 | debug('Table "%s" check failed as expected', table) |
|---|
| 162 | return True |
|---|
| 163 | else: |
|---|
| 164 | debug('Table "%s" needs upgrade: %s', table, e) |
|---|
| 165 | return False |
|---|
| 166 | else: |
|---|
| 167 | if should_fail: |
|---|
| 168 | debug('Table "%s" needs upgrade', table) |
|---|
| 169 | return False |
|---|
| 170 | else: |
|---|
| 171 | debug('Table "%s" has the right structure', table) |
|---|
| 172 | return True |
|---|
| 173 | |
|---|
| 174 | return not ( |
|---|
| 175 | # <0.9 had a "name" field |
|---|
| 176 | check('darcs_changesets', |
|---|
| 177 | 'SELECT repo_id,rev,hash,name ' |
|---|
| 178 | 'FROM darcs_changesets LIMIT 1', should_fail=True) and |
|---|
| 179 | check('darcs_changesets', |
|---|
| 180 | 'SELECT repo_id,rev,hash ' |
|---|
| 181 | 'FROM darcs_changesets LIMIT 1') and |
|---|
| 182 | check('darcs_nodes', |
|---|
| 183 | 'SELECT repo_id,node_id,node_type,add_rev,remove_rev ' |
|---|
| 184 | 'FROM darcs_nodes LIMIT 1') and |
|---|
| 185 | check('darcs_node_changes', |
|---|
| 186 | 'SELECT repo_id,node_id,rev,path,parent_id,change_kind ' |
|---|
| 187 | 'FROM darcs_node_changes LIMIT 1') and |
|---|
| 188 | check('darcs_cache', |
|---|
| 189 | 'SELECT repo_id,node_id,rev,content,size ' |
|---|
| 190 | 'FROM darcs_cache LIMIT 1') and |
|---|
| 191 | check('darcs_annotate_cache', |
|---|
| 192 | 'SELECT repo_id,node_id,rev ' |
|---|
| 193 | 'FROM darcs_annotate_cache LIMIT 1')) |
|---|
| 194 | |
|---|
| 195 | def upgrade_environment(self, db): |
|---|
| 196 | """Actually add the new db tables.""" |
|---|
| 197 | |
|---|
| 198 | def drop_table(table_name): |
|---|
| 199 | c = db.cursor() |
|---|
| 200 | try: |
|---|
| 201 | c.execute('drop table %s' % table_name) |
|---|
| 202 | except: |
|---|
| 203 | db.rollback() |
|---|
| 204 | pass |
|---|
| 205 | |
|---|
| 206 | drop_table('darcs_revisions') # until 0.6 |
|---|
| 207 | drop_table('darcs_changesets') |
|---|
| 208 | drop_table('darcs_nodes') |
|---|
| 209 | drop_table('darcs_node_changes') |
|---|
| 210 | drop_table('darcs_cache') |
|---|
| 211 | drop_table('darcs_annotate_cache') |
|---|
| 212 | |
|---|
| 213 | connector = DatabaseManager(self.env)._get_connector()[0] |
|---|
| 214 | if 'postgres' in [supp[0] for supp in connector.get_supported_schemes()]: |
|---|
| 215 | blobtype = 'bytea' |
|---|
| 216 | else: |
|---|
| 217 | blobtype = 'blob' |
|---|
| 218 | rev_table = Table('darcs_changesets', key=('repo_id','rev'))[ |
|---|
| 219 | Column('repo_id',type='int'), |
|---|
| 220 | Column('rev',type='int'), |
|---|
| 221 | Column('hash'), |
|---|
| 222 | Index(['hash','repo_id'])] |
|---|
| 223 | node_table = Table('darcs_nodes', key=('repo_id','node_id'))[ |
|---|
| 224 | Column('repo_id',type='int'), |
|---|
| 225 | Column('node_id',type='int'), |
|---|
| 226 | Column('node_type',size=1), |
|---|
| 227 | Column('add_rev',type='int'), |
|---|
| 228 | Column('remove_rev',type='int')] |
|---|
| 229 | change_table = Table('darcs_node_changes', key=('repo_id','node_id','rev'))[ |
|---|
| 230 | Column('repo_id',type='int'), |
|---|
| 231 | Column('node_id',type='int'), |
|---|
| 232 | Column('rev',type='int'), |
|---|
| 233 | Column('path'), |
|---|
| 234 | Column('parent_id',type='int'), |
|---|
| 235 | Column('change_kind'), |
|---|
| 236 | Index(['path', 'repo_id'])] |
|---|
| 237 | cache_table = Table('darcs_cache', key=('repo_id','node_id','rev'))[ |
|---|
| 238 | Column('repo_id',type='int'), |
|---|
| 239 | Column('node_id',type='int'), |
|---|
| 240 | Column('rev',type='int'), |
|---|
| 241 | Column('content',type=blobtype), |
|---|
| 242 | Column('size',type='int')] |
|---|
| 243 | ann_cache_table = Table('darcs_annotate_cache', key=('repo_id','node_id','rev','up_to_line'))[ |
|---|
| 244 | Column('repo_id',type='int'), |
|---|
| 245 | Column('node_id',type='int'), |
|---|
| 246 | Column('rev',type='int'), |
|---|
| 247 | Column('up_to_line',type='int'), |
|---|
| 248 | Column('blame_rev',type='int')] |
|---|
| 249 | c = db.cursor() |
|---|
| 250 | for t in [rev_table,node_table,change_table,cache_table,ann_cache_table]: |
|---|
| 251 | for stmt in connector.to_sql(t): |
|---|
| 252 | c.execute(stmt) |
|---|
| 253 | |
|---|
| 254 | # IAdminCommandProvider methods |
|---|
| 255 | |
|---|
| 256 | def get_admin_commands(self): |
|---|
| 257 | yield ('repository identity', '<repos> [identity]', |
|---|
| 258 | 'Get or set the identity tag of a repository', |
|---|
| 259 | self._complete_repos, self._do_repository_identity) |
|---|
| 260 | |
|---|
| 261 | def _get_reponames(self): |
|---|
| 262 | rm = RepositoryManager(self.env) |
|---|
| 263 | return [reponame or '(default)' for reponame |
|---|
| 264 | in rm.get_all_repositories()] |
|---|
| 265 | |
|---|
| 266 | def _complete_repos(self, args): |
|---|
| 267 | if len(args) == 1: |
|---|
| 268 | return self._get_reponames() |
|---|
| 269 | |
|---|
| 270 | def _do_repository_identity(self, reponame, identity=None): |
|---|
| 271 | rm = RepositoryManager(self.env) |
|---|
| 272 | repo = rm.get_repository(reponame) |
|---|
| 273 | if repo is None: |
|---|
| 274 | printout("Invalid repository!") |
|---|
| 275 | return |
|---|
| 276 | |
|---|
| 277 | repoid = repo.id |
|---|
| 278 | |
|---|
| 279 | db = self.env.get_db_cnx() |
|---|
| 280 | cursor = db.cursor() |
|---|
| 281 | |
|---|
| 282 | cursor.execute("SELECT value FROM repository " |
|---|
| 283 | "WHERE id=%s AND name='identity'", (repoid,)) |
|---|
| 284 | row = cursor.fetchone() |
|---|
| 285 | |
|---|
| 286 | if identity is None: |
|---|
| 287 | if row is not None: |
|---|
| 288 | printout("Identity of repository %s: %s" % |
|---|
| 289 | (reponame, row[0])) |
|---|
| 290 | else: |
|---|
| 291 | printout("No identity set on repository %s" % |
|---|
| 292 | reponame) |
|---|
| 293 | else: |
|---|
| 294 | @self.env.with_transaction() |
|---|
| 295 | def set_identity(db): |
|---|
| 296 | if row is None and identity: |
|---|
| 297 | cursor.execute("INSERT INTO repository (id, name, value) " |
|---|
| 298 | "VALUES (%s, 'identity', %s)", (repoid, identity)) |
|---|
| 299 | printout("Identity of repository %s set to %s" % |
|---|
| 300 | (reponame, identity)) |
|---|
| 301 | elif not identity: |
|---|
| 302 | if row is not None: |
|---|
| 303 | cursor.execute("DELETE FROM repository " |
|---|
| 304 | "WHERE id=%s AND name='identity'", (repoid,)) |
|---|
| 305 | printout("Identity removed from repository %s" % |
|---|
| 306 | reponame) |
|---|
| 307 | elif identity != row[0]: |
|---|
| 308 | cursor.execute("UPDATE repository SET value=%s " |
|---|
| 309 | "WHERE id=%s AND name='identity'", |
|---|
| 310 | (identity, repoid)) |
|---|
| 311 | printout("Identity of repository %s set to %s " |
|---|
| 312 | "(was %s)" % (reponame, identity, row[0])) |
|---|
| 313 | else: |
|---|
| 314 | printout("Identity of repository %s is already set to %s" |
|---|
| 315 | % (reponame, identity)) |
|---|
| 316 | |
|---|
| 317 | |
|---|
| 318 | class EquivalentChangesetsRenderer(Component): |
|---|
| 319 | """Handle the `PresentIn` changesets property.""" |
|---|
| 320 | |
|---|
| 321 | implements(IPropertyRenderer) |
|---|
| 322 | |
|---|
| 323 | def match_property(self, name, mode): |
|---|
| 324 | return (mode == 'revprop' and name == 'PresentIn') and 5 or 0 |
|---|
| 325 | |
|---|
| 326 | def render_property(self, name, mode, context, props): |
|---|
| 327 | eqcsets = props[name] |
|---|
| 328 | eqlinks = [(tag.a(repos or '(default)', class_="changeset", |
|---|
| 329 | title="Equivalent patch %s in repository %s" % ( |
|---|
| 330 | rev, repos or '(default)'), |
|---|
| 331 | href=context.href.changeset(rev, repos)),) |
|---|
| 332 | for repos, rev in eqcsets] |
|---|
| 333 | return RenderedProperty(name='Present in:', |
|---|
| 334 | name_attributes=[("class", "property")], |
|---|
| 335 | content=tag([(link, ', ') for link in eqlinks[:-1]], |
|---|
| 336 | eqlinks[-1])) |
|---|
| 337 | |
|---|
| 338 | |
|---|
| 339 | class MissingInReposRenderer(Component): |
|---|
| 340 | """Handle the `MissingIn` changesets property.""" |
|---|
| 341 | |
|---|
| 342 | implements(IPropertyRenderer) |
|---|
| 343 | |
|---|
| 344 | def match_property(self, name, mode): |
|---|
| 345 | return (mode == 'revprop' and name == 'MissingIn') and 5 or 0 |
|---|
| 346 | |
|---|
| 347 | def render_property(self, name, mode, context, props): |
|---|
| 348 | mir = props[name] |
|---|
| 349 | return RenderedProperty(name='Missing in:', |
|---|
| 350 | name_attributes=[("class", "property")], |
|---|
| 351 | content=tag([(repo, ', ') for repo in mir[:-1]], |
|---|
| 352 | mir[-1])) |
|---|
| 353 | |
|---|
| 354 | |
|---|
| 355 | class HashnameRenderer(Component): |
|---|
| 356 | """Handle the `Hashname` changesets property.""" |
|---|
| 357 | |
|---|
| 358 | implements(IPropertyRenderer) |
|---|
| 359 | |
|---|
| 360 | def match_property(self, name, mode): |
|---|
| 361 | return (mode == 'revprop' and name == 'Hashname') and 5 or 0 |
|---|
| 362 | |
|---|
| 363 | def render_property(self, name, mode, context, props): |
|---|
| 364 | hash = props[name] |
|---|
| 365 | repos = context.resource.parent.id |
|---|
| 366 | link = tag.a(hash, class_="changeset", |
|---|
| 367 | title="Permanent link to this changeset", |
|---|
| 368 | href=context.href.changeset(hash, repos)) |
|---|
| 369 | return RenderedProperty(name='Hash name:', |
|---|
| 370 | name_attributes=[("class", "property")], |
|---|
| 371 | content=link) |
|---|