source: tracdarcs/tracdarcs/components.py @ 242

Revision 242, 14.5 KB checked in by lele@…, 6 months ago (diff)

Split log lines and tweak whitespace

Line 
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
16from genshi.builder import tag
17
18from trac.admin import IAdminCommandProvider
19from trac.config import BoolOption, Option
20from trac.core import Component, implements
21from trac.db import Column, DatabaseManager, Index, Table
22from trac.env import IEnvironmentSetupParticipant
23from trac.util.text import printout, shorten_line, to_unicode
24from trac.versioncontrol import IRepositoryConnector, NoSuchChangeset, \
25                                RepositoryManager
26from trac.versioncontrol.web_ui import IPropertyRenderer, RenderedProperty
27
28from trac.wiki import IWikiSyntaxProvider
29
30from tracdarcs.repository import DarcsRepository
31
32
33class 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
132class 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
318class 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
339class 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
355class 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)
Note: See TracBrowser for help on using the repository browser.