Ticket #986: trac_mysql_r2659.patch
| File trac_mysql_r2659.patch, 27.2 kB (added by trac@…, 3 years ago) |
|---|
-
trac/db_default.py
18 18 from trac.db import Table, Column, Index 19 19 20 20 # Database version identifier. Used for automatic upgrades. 21 db_version = 1 621 db_version = 17 22 22 23 23 def __mkreports(reports): 24 24 """Utility function used to create report data in same syntax as the … … 83 83 Column('author'), 84 84 Column('message'), 85 85 Index(['time'])], 86 Table('node_change', key=('rev', 'path', 'change '))[86 Table('node_change', key=('rev', 'path', 'change_type'))[ 87 87 Column('rev'), 88 88 Column('path'), 89 Column(' kind', size=1),90 Column('change ', size=1),89 Column('node_type', size=1), 90 Column('change_type', size=1), 91 91 Column('base_path'), 92 92 Column('base_rev'), 93 93 Index(['rev'])], … … 437 437 ) 438 438 439 439 default_components = ('trac.About', 'trac.attachment', 440 'trac.db.mysql_backend', 440 441 'trac.db.postgres_backend', 'trac.db.sqlite_backend', 441 442 'trac.mimeview.enscript', 'trac.mimeview.patch', 442 443 'trac.mimeview.php', 'trac.mimeview.rst', -
trac/ticket/api.py
167 167 return 168 168 db = self.env.get_db_cnx() 169 169 sql, args = query_to_sql(db, query, 'b.newvalue') 170 sql2, args2 = query_to_sql(db, query, 'summary||keywords||description||reporter||cc') 170 search_fields = ['summary', 'keywords', 'description', 'reporter', 'cc'] 171 sql_seq = [] 172 for f in search_fields: 173 subsql, subarg = query_to_sql(db, query, f) 174 sql_seq.append(subsql) 175 args += subarg 176 sql2 = ' OR '.join(sql_seq) 171 177 cursor = db.cursor() 172 cursor.execute("SELECT DISTINCT a.summary,a.description,a.reporter, "173 "a.keywords,a.id,a.time FROM ticket a " 174 "LEFT JOIN ticket_change b ON a.id = b.ticket " 175 "WHERE (b.field='comment' AND %s ) OR %s" % (sql, sql2) ,176 args + args2)178 sql3 = "SELECT DISTINCT a.summary,a.description,a.reporter, " + \ 179 "a.keywords,a.id,a.time FROM ticket a " + \ 180 "LEFT JOIN ticket_change b ON a.id = b.ticket " + \ 181 "WHERE (b.field='comment' AND %s ) OR %s" % (sql, sql2) 182 cursor.execute(sql3, args) 177 183 for summary,desc,author,keywords,tid,date in cursor: 178 184 yield (self.env.href.ticket(tid), 179 185 '#%d: %s' % (tid, util.escape(util.shorten_line(summary))), -
trac/db/tests/api.py
39 39 'host': 'localhost', 'port': 9431, 40 40 'path': '/trac'}), 41 41 _parse_db_str('postgres://john:letmein@localhost:9431/trac')) 42 def test_mysql_simple(self): 43 self.assertEqual(('mysql', {'host': 'localhost', 'path': '/trac'}), 44 _parse_db_str('mysql://localhost/trac')) 42 45 43 46 def test_mysql_with_creds(self): 47 self.assertEqual(('mysql', {'user': 'john', 'password': 'letmein', 48 'host': 'localhost', 'port': 3306, 49 'path': '/trac'}), 50 _parse_db_str('mysql://john:letmein@localhost:3306/trac')) 44 51 def suite(): 45 52 return unittest.makeSuite(ParseConnectionStringTestCase,'test') 46 53 -
trac/db/mysql_backend.py
1 # -*- coding: iso8859-1 -*- 2 # 3 # Copyright (C) 2005 Edgewall Software 4 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de> 5 # Copyright (C) 2005 Jeff Weiss <trac@jeffweiss.org> 6 # All rights reserved. 7 # 8 # This software is licensed as described in the file COPYING, which 9 # you should have received as part of this distribution. The terms 10 # are also available at http://trac.edgewall.com/license.html. 11 # 12 # This software consists of voluntary contributions made by many 13 # individuals. For the exact contribution history, see the revision 14 # history and logs, available at http://projects.edgewall.com/trac/. 15 # 16 # Derived from postgres_backend.py 17 # Author: Christopher Lenz <cmlenz@gmx.de> 18 # Author: Jeff Weiss <trac@jeffweiss.org> 19 20 from trac.core import * 21 from trac.db.api import IDatabaseConnector 22 from trac.db.util import ConnectionWrapper 23 24 import MySQLdb 25 26 class MySQLConnector(Component): 27 """MySQL database support. Still extremely experimental! 28 Currently only supports database urls of the following formats: 29 mysql://user:password@host/database 30 mysql://user:password@host:port/database 31 """ 32 33 implements(IDatabaseConnector) 34 35 def get_supported_schemes(self): 36 return [('mysql', 1)] 37 38 def get_connection(self, path, user=None, password=None, host=None, 39 port=None, params={}): 40 return MySQLConnection(path, user, password, host, port, params) 41 42 def init_db(self, path, user=None, password=None, host=None, port=None, 43 params={}): 44 cnx = self.get_connection(path, user, password, host, port, params) 45 cursor = cnx.cursor() 46 from trac.db_default import schema 47 for table in schema: 48 for stmt in self.to_sql(table): 49 self.env.log.debug(stmt) 50 cursor.execute(stmt) 51 cnx.commit() 52 53 def to_sql(self, table): 54 sql = ["CREATE TABLE %s (" % table.name] 55 coldefs = [] 56 textkeys = [] 57 textcols = [] 58 mytablekey = [] 59 num_key_text = 0 60 for column in table.columns: 61 ctype = column.type 62 if column.auto_increment: 63 ctype = "INT AUTO_INCREMENT" 64 # Overide the column type because otherwise it will screw 65 # up the rest of the code for indexes and constraints 66 column.type = 'int' 67 if column.name in table.key: 68 mytablekey.append(column.name) 69 if column.type == 'text': 70 num_key_text += 1 71 textkeys.append(column.name) 72 if column.type == 'text': 73 textcols.append(column.name) 74 coldefs.append(" `%s` %s" % (column.name, ctype)) 75 if len(table.key) >= 1: 76 if num_key_text == 0: 77 num_key_text = 1 78 size = 767 / num_key_text 79 keysize = "(%s)" % size 80 for key in textkeys: 81 if key in mytablekey: 82 mytablekey.remove(key) 83 sizedkey = '`'+key+'`' + keysize 84 mytablekey.append(sizedkey) 85 coldefs.append(" CONSTRAINT %s_pk PRIMARY KEY (%s)" 86 % (table.name, ','.join(mytablekey))) 87 sql.append(',\n'.join(coldefs) + '\n)') 88 yield '\n'.join(sql) 89 # This does not work because what I need to do is not add something about 90 # the index size for the text fields because MySQL apparrently can't index 91 # on the entire length of the text field 92 for index in table.indices: 93 myidxcols = set([]) 94 processedidx = set([]) 95 for col in index.columns: 96 if col in textcols: 97 myidxcols.add(col) 98 myidx = set(index.columns) - myidxcols 99 for col in myidx: 100 escaped_col = '`'+col+'`' 101 processedidx.add(escaped_col) 102 if len(myidxcols) > 0: 103 size = 767 / len(myidxcols) 104 idxsize = "(%s)" % size 105 for col in myidxcols: 106 sizedidx = '`'+col+'`'+idxsize 107 processedidx.add(sizedidx) 108 yield "CREATE INDEX %s_%s_idx ON %s (%s)" % (table.name, 109 '_'.join(index.columns), table.name, ','.join(processedidx)) 110 111 112 class MySQLConnection(ConnectionWrapper): 113 """Connection wrapper for MySQL.""" 114 115 poolable = True 116 117 def __init__(self, path, usr=None, password=None, hst=None, prt=None, 118 params={}): 119 if prt: 120 cnx = MySQLdb.connect(db=path[1:], user=usr, passwd=password, host=hst, port=prt) 121 else: 122 cnx = MySQLdb.connect(db=path[1:], user=usr, passwd=password, host=hst) 123 124 ConnectionWrapper.__init__(self, cnx) 125 126 def cast(self, column, type): 127 # Temporary hack needed for the union of selects in the search module 128 return 'CAST(%s AS %s)' % (column, type) 129 130 def like(self): 131 # Temporary hack needed for the case-insensitive string matching in the 132 # search module 133 return 'LIKE' 134 135 def get_last_id(self, cursor, table, column='id'): 136 sql = "SELECT MAX(%s) FROM %s" % (column, table) 137 cursor.execute(sql) 138 return cursor.fetchone()[0] -
trac/versioncontrol/api.py
132 132 DIRECTORY = "dir" 133 133 FILE = "file" 134 134 135 def __init__(self, path, rev, kind):136 assert kind in (Node.DIRECTORY, Node.FILE), "Unknown node kind %s" % kind135 def __init__(self, path, rev, node_type): 136 assert node_type in (Node.DIRECTORY, Node.FILE), "Unknown node node_type %s" % node_type 137 137 self.path = str(path) 138 138 self.rev = rev 139 self. kind = kind139 self.node_type = node_type 140 140 141 141 def get_content(self): 142 142 """ … … 187 187 raise NotImplementedError 188 188 last_modified = property(lambda x: x.get_last_modified()) 189 189 190 isdir = property(lambda x: x. kind== Node.DIRECTORY)191 isfile = property(lambda x: x. kind== Node.FILE)190 isdir = property(lambda x: x.node_type == Node.DIRECTORY) 191 isfile = property(lambda x: x.node_type == Node.FILE) 192 192 193 193 194 194 class Changeset(object): … … 210 210 211 211 def get_changes(self): 212 212 """ 213 Generator that produces a (path, kind, change, base_rev, base_path)213 Generator that produces a (path, node_type, change_type, base_rev, base_path) 214 214 tuple for every change in the changeset, where change can be one of 215 215 Changeset.ADD, Changeset.COPY, Changeset.DELETE, Changeset.EDIT or 216 Changeset.MOVE, and kindis one of Node.FILE or Node.DIRECTORY.216 Changeset.MOVE, and node_type is one of Node.FILE or Node.DIRECTORY. 217 217 """ 218 218 raise NotImplementedError 219 219 -
trac/versioncontrol/tests/svn_fs.py
117 117 node = self.repos.get_node('/trunk') 118 118 self.assertEqual('trunk', node.name) 119 119 self.assertEqual('/trunk', node.path) 120 self.assertEqual(Node.DIRECTORY, node. kind)120 self.assertEqual(Node.DIRECTORY, node.node_type) 121 121 self.assertEqual(14, node.rev) 122 122 self.assertEqual(1133340423L, node.last_modified) 123 123 node = self.repos.get_node('/trunk/README.txt') 124 124 self.assertEqual('README.txt', node.name) 125 125 self.assertEqual('/trunk/README.txt', node.path) 126 self.assertEqual(Node.FILE, node. kind)126 self.assertEqual(Node.FILE, node.node_type) 127 127 self.assertEqual(3, node.rev) 128 128 self.assertEqual(1112361898, node.last_modified) 129 129 … … 131 131 node = self.repos.get_node('/trunk', 1) 132 132 self.assertEqual('trunk', node.name) 133 133 self.assertEqual('/trunk', node.path) 134 self.assertEqual(Node.DIRECTORY, node. kind)134 self.assertEqual(Node.DIRECTORY, node.node_type) 135 135 self.assertEqual(1, node.rev) 136 136 self.assertEqual(1112349652, node.last_modified) 137 137 node = self.repos.get_node('/trunk/README.txt', 2) 138 138 self.assertEqual('README.txt', node.name) 139 139 self.assertEqual('/trunk/README.txt', node.path) 140 self.assertEqual(Node.FILE, node. kind)140 self.assertEqual(Node.FILE, node.node_type) 141 141 self.assertEqual(2, node.rev) 142 142 self.assertEqual(1112361138, node.last_modified) 143 143 … … 370 370 node = self.repos.get_node('/dir1') 371 371 self.assertEqual('dir1', node.name) 372 372 self.assertEqual('/dir1', node.path) 373 self.assertEqual(Node.DIRECTORY, node. kind)373 self.assertEqual(Node.DIRECTORY, node.node_type) 374 374 self.assertEqual(5, node.rev) 375 375 self.assertEqual(1112372739, node.last_modified) 376 376 node = self.repos.get_node('/README.txt') 377 377 self.assertEqual('README.txt', node.name) 378 378 self.assertEqual('/README.txt', node.path) 379 self.assertEqual(Node.FILE, node. kind)379 self.assertEqual(Node.FILE, node.node_type) 380 380 self.assertEqual(3, node.rev) 381 381 self.assertEqual(1112361898, node.last_modified) 382 382 … … 384 384 node = self.repos.get_node('/dir1', 4) 385 385 self.assertEqual('dir1', node.name) 386 386 self.assertEqual('/dir1', node.path) 387 self.assertEqual(Node.DIRECTORY, node. kind)387 self.assertEqual(Node.DIRECTORY, node.node_type) 388 388 self.assertEqual(4, node.rev) 389 389 self.assertEqual(1112370155, node.last_modified) 390 390 node = self.repos.get_node('/README.txt', 2) 391 391 self.assertEqual('README.txt', node.name) 392 392 self.assertEqual('/README.txt', node.path) 393 self.assertEqual(Node.FILE, node. kind)393 self.assertEqual(Node.FILE, node.node_type) 394 394 self.assertEqual(2, node.rev) 395 395 self.assertEqual(1112361138, node.last_modified) 396 396 -
trac/versioncontrol/tests/cache.py
66 66 self.assertEquals(('0', 41000, '', ''), cursor.fetchone()) 67 67 self.assertEquals(('1', 42000, 'joe', 'Import'), cursor.fetchone()) 68 68 self.assertEquals(None, cursor.fetchone()) 69 cursor.execute("SELECT rev,path, kind,change,base_path,base_rev "69 cursor.execute("SELECT rev,path,node_type,change_type,base_path,base_rev " 70 70 "FROM node_change") 71 71 self.assertEquals(('1', 'trunk', 'D', 'A', None, None), 72 72 cursor.fetchone()) … … 80 80 "VALUES (0,41000,'','')") 81 81 cursor.execute("INSERT INTO revision (rev,time,author,message) " 82 82 "VALUES (1,42000,'joe','Import')") 83 cursor.executemany("INSERT INTO node_change (rev,path, kind,change,"83 cursor.executemany("INSERT INTO node_change (rev,path,node_type,change_type," 84 84 "base_path,base_rev) VALUES ('1',%s,%s,%s,%s,%s)", 85 85 [('trunk', 'D', 'A', None, None), 86 86 ('trunk/README', 'F', 'A', None, None)]) … … 99 99 cursor.execute("SELECT time,author,message FROM revision WHERE rev='2'") 100 100 self.assertEquals((42042, 'joe', 'Update'), cursor.fetchone()) 101 101 self.assertEquals(None, cursor.fetchone()) 102 cursor.execute("SELECT path, kind,change,base_path,base_rev "102 cursor.execute("SELECT path,node_type,change_type,base_path,base_rev " 103 103 "FROM node_change WHERE rev='2'") 104 104 self.assertEquals(('trunk/README', 'F', 'E', 'trunk/README', '1'), 105 105 cursor.fetchone()) … … 111 111 "VALUES (0,41000,'','')") 112 112 cursor.execute("INSERT INTO revision (rev,time,author,message) " 113 113 "VALUES (1,42000,'joe','Import')") 114 cursor.executemany("INSERT INTO node_change (rev,path, kind,change,"114 cursor.executemany("INSERT INTO node_change (rev,path,node_type,change_type," 115 115 "base_path,base_rev) VALUES ('1',%s,%s,%s,%s,%s)", 116 116 [('trunk', 'D', 'A', None, None), 117 117 ('trunk/README', 'F', 'A', None, None)]) -
trac/versioncontrol/web_ui/changeset.py
182 182 183 183 edits = [] 184 184 idx = 0 185 for path, kind, change, base_path, base_rev in chgset.get_changes():186 info = {'change': change }185 for path, node_type, change_type, base_path, base_rev in chgset.get_changes(): 186 info = {'change': change_type} 187 187 if base_path: 188 188 info['path.old'] = base_path 189 189 info['rev.old'] = base_rev … … 194 194 info['rev.new'] = chgset.rev 195 195 info['browser_href.new'] = self.env.href.browser(path, 196 196 rev=chgset.rev) 197 if change in (Changeset.COPY, Changeset.EDIT, Changeset.MOVE):198 edits.append((idx, path, kind, base_path, base_rev))197 if change_type in (Changeset.COPY, Changeset.EDIT, Changeset.MOVE): 198 edits.append((idx, path, node_type, base_path, base_rev)) 199 199 req.hdf['changeset.changes.%d' % idx] = info 200 200 idx += 1 201 201 … … 205 205 206 206 mimeview = Mimeview(self.env) 207 207 208 for idx, path, kind, base_path, base_rev in edits:208 for idx, path, node_type, base_path, base_rev in edits: 209 209 old_node = repos.get_node(base_path or path, base_rev) 210 210 new_node = repos.get_node(path, chgset.rev) 211 211 … … 227 227 del changed_props[k] 228 228 req.hdf['changeset.changes.%d.props' % idx] = changed_props 229 229 230 if kind== Node.DIRECTORY:230 if node_type == Node.DIRECTORY: 231 231 continue 232 232 233 233 # Content changes … … 269 269 req.end_headers() 270 270 271 271 mimeview = Mimeview(self.env) 272 273 for path, kind, change, base_path, base_rev in chgset.get_changes(): 274 if change == Changeset.ADD: 272 for path, node_type, change_type, base_path, base_rev in chgset.get_changes(): 273 if change_type == Changeset.ADD: 275 274 old_node = None 276 275 else: 277 276 old_node = repos.get_node(base_path or path, base_rev) 278 if change == Changeset.DELETE:277 if change_type == Changeset.DELETE: 279 278 new_node = None 280 279 else: 281 280 new_node = repos.get_node(path, chgset.rev) … … 283 282 # TODO: Property changes 284 283 285 284 # Content changes 286 if kind== 'dir':285 if node_type == 'dir': 287 286 continue 288 287 289 288 new_content = old_content = '' … … 338 337 339 338 buf = StringIO() 340 339 zipfile = ZipFile(buf, 'w', ZIP_DEFLATED) 341 for path, kind, change, base_path, base_rev in chgset.get_changes():342 if kind == Node.FILE and change != Changeset.DELETE:340 for path, node_type, change_type, base_path, base_rev in chgset.get_changes(): 341 if node_type == Node.FILE and change_type != Changeset.DELETE: 343 342 node = repos.get_node(path, chgset.rev) 344 343 zipinfo = ZipInfo() 345 344 zipinfo.filename = node.path … … 383 382 return 384 383 authzperm = SubversionAuthorizer(self.env, req.authname) 385 384 db = self.env.get_db_cnx() 386 sql, args = query_to_sql(db, query, 'message||author') 385 query_fields = ['message', 'author'] 386 sql_seq = [] 387 args = [] 388 for f in query_fields: 389 subsql, subarg = query_to_sql(db, query, f) 390 sql_seq.append(subsql) 391 args += subarg 392 sql = ' OR '.join(sql_seq) 387 393 cursor = db.cursor() 388 394 cursor.execute("SELECT rev,time,author,message " 389 395 "FROM revision WHERE " + sql, args) -
trac/versioncontrol/cache.py
18 18 from trac.versioncontrol import Changeset, Node, Repository, Authorizer 19 19 20 20 21 _ kindmap = {'D': Node.DIRECTORY, 'F': Node.FILE}22 _ actionmap = {'A': Changeset.ADD, 'C': Changeset.COPY,21 _node_typemap = {'D': Node.DIRECTORY, 'F': Node.FILE} 22 _change_typemap = {'A': Changeset.ADD, 'C': Changeset.COPY, 23 23 'D': Changeset.DELETE, 'E': Changeset.EDIT, 24 24 'M': Changeset.MOVE} 25 25 … … 64 64 authz = self.repos.authz 65 65 self.repos.authz = Authorizer() # remove permission checking 66 66 67 kindmap = dict(zip(_kindmap.values(), _kindmap.keys()))68 actionmap = dict(zip(_actionmap.values(), _actionmap.keys()))67 node_typemap = dict(zip(_node_typemap.values(), _node_typemap.keys())) 68 change_typemap = dict(zip(_change_typemap.values(), _change_typemap.keys())) 69 69 self.log.info("Syncing with repository (%s to %s)" 70 70 % (youngest_stored, self.repos.youngest_rev)) 71 71 if youngest_stored: … … 78 78 "VALUES (%s,%s,%s,%s)", (str(current_rev), 79 79 changeset.date, changeset.author, 80 80 changeset.message)) 81 for path, kind,action,base_path,base_rev in changeset.get_changes():81 for path,node_type,change_type,base_path,base_rev in changeset.get_changes(): 82 82 self.log.debug("Caching node change in [%s]: %s" 83 % (current_rev, (path, kind, action,83 % (current_rev, (path, node_type, change_type, 84 84 base_path, base_rev))) 85 kind = kindmap[kind]86 action = actionmap[action]87 cursor.execute("INSERT INTO node_change (rev,path, kind,"88 "change ,base_path,base_rev) "85 node_type = node_typemap[node_type] 86 change_type = change_typemap[change_type] 87 cursor.execute("INSERT INTO node_change (rev,path,node_type," 88 "change_type,base_path,base_rev) " 89 89 "VALUES (%s,%s,%s,%s,%s,%s)", 90 (str(current_rev), path, kind, action,90 (str(current_rev), path, node_type, change_type, 91 91 base_path, base_rev)) 92 92 current_rev = self.repos.next_rev(current_rev) 93 93 self.db.commit() … … 141 141 142 142 def get_changes(self): 143 143 cursor = self.db.cursor() 144 cursor.execute("SELECT path, kind,change,base_path,base_rev "144 cursor.execute("SELECT path,node_type,change_type,base_path,base_rev " 145 145 "FROM node_change WHERE rev=%s " 146 146 "ORDER BY path", (self.rev,)) 147 for path, kind, change, base_path, base_rev in cursor:147 for path, node_type, change_type, base_path, base_rev in cursor: 148 148 if not self.authz.has_permission(path): 149 149 # FIXME: what about the base_path? 150 150 continue 151 kind = _kindmap[kind]152 change = _actionmap[change]153 yield path, kind, change, base_path, base_rev151 node_type = _node_typemap[node_type] 152 change_type = _change_typemap[change_type] 153 yield path, node_type, change_type, base_path, base_rev -
trac/upgrades/db17.py
1 from trac.db import Table, Column, Index, DatabaseManager 2 3 def do_upgrade(env, ver, cursor): 4 cursor.execute("CREATE TEMP TABLE node_change_old AS SELECT * FROM node_change") 5 cursor.execute("DROP TABLE node_change") 6 7 db = env.get_db_cnx() 8 node_change_table = Table('node_change', key=('rev', 'path', 'change_type'))[ 9 Column('rev'), 10 Column('path'), 11 Column('node_type', size=1), 12 Column('change_type', size=1), 13 Column('base_path'), 14 Column('base_rev'), 15 Index(['rev'])] 16 db_backend, _ = DatabaseManager(env)._get_connector() 17 for stmt in db_backend.to_sql(node_change_table): 18 cursor.execute(stmt) 19 20 cursor.execute("INSERT INTO node_change (rev,path,node_type,change_type,base_path,base_rev) " 21 "SELECT rev,path,kind,change,base_path,base_rev " 22 "FROM node_change_old") -
trac/web/session.py
181 181 # changed as to minimize the purging. 182 182 mintime = now - PURGE_AGE 183 183 self.env.log.debug('Purging old, expired, sessions.') 184 cursor.execute("DELETE FROM session WHERE authenticated=0 AND " 185 "sid IN (SELECT sid FROM session WHERE " 186 "var_name='last_visit' AND var_value < %s)", 184 # This is a temporary hack because MySQL doesn't support 185 # a delete statement with the subquery coming from the same 186 # table 187 cursor.execute("SELECT sid from session WHERE " 188 "var_name='last_visit' AND var_value < %s", 187 189 (mintime,)) 188 190 sids = cursor.fetchall() 191 if sids and len(sids) >= 1: 192 vals = [] 193 sids_args = [] 194 for t in sids: 195 vals.append(t[0]) 196 sids_args.append("sid = %s") 197 sql = "DELETE FROM session WHERE authenticated = 0 AND (%s)" % ' OR '.join(sids_args) 198 cursor.execute(sql, vals) 189 199 db.commit() -
templates/changeset.cs
70 70 </div> 71 71 </form><?cs /if ?> 72 72 73 <?cs def:node_change(item,cl, kind) ?><?cs73 <?cs def:node_change(item,cl,node_type) ?><?cs 74 74 set:ndiffs = len(item.diff) ?><?cs 75 75 set:nprops = len(item.props) ?> 76 76 <div class="<?cs var:cl ?>"></div><?cs … … 81 81 <a title="Show entry in browser" href="<?cs 82 82 var:item.browser_href.new ?>"><?cs var:item.path.new ?></a><?cs 83 83 /if ?> 84 <span class="comment">(<?cs var: kind?>)</span><?cs84 <span class="comment">(<?cs var:node_type ?>)</span><?cs 85 85 if:item.path.old && item.change == 'copy' || item.change == 'move' ?> 86 <small><em>(<?cs var: kind?> from <a href="<?cs86 <small><em>(<?cs var:node_type ?> from <a href="<?cs 87 87 var:item.browser_href.old ?>" title="Show original file (rev. <?cs 88 88 var:item.rev.old ?>)"><?cs var:item.path.old ?></a>)</em></small><?cs 89 89 /if ?><?cs
